require "ftools"
require "erb"
require File.dirname(__FILE__) + '/RCova'

module QCov
module Profile

	class FatalError < Exception
	end
	
	class ParseFeed
		def initialize excludes, &block
			@excludes = excludes
			@block = block
			
			reset_module
			reset_file		
		end
		
		
		def reset_module
			@module = ""
		end
		
		def reset_file
			@bypass_file = false
			
			@file = ""
			@lines = []
			
			@covered = 0
			@uncovered = 0
			@noop = 0
		end
		
		def yield_lines_out
			if not @lines.empty?
				@block.call(:module => @module, :name => @file, :lines => @lines, :total => @lines.size, 
					:covered => @covered, :uncovered => @uncovered, :noop => @noop, 
					:coverage => ((@covered * 10000) / (@covered + @uncovered)).to_i )
			end
		end
		
		def module name, lines, coverage
			reset_module
			@module = name
		end
		
		def file name
			yield_lines_out
			reset_file
			
			@excludes.each do |e|
				if name =~ e then
					@bypass_file = true
					return
				end
			end
			@file = name
		end
		
		def line lineno, lc, text
			return if @bypass_file
			@lines << {:lineno => lineno, :status => lc, :text => text}
			case lc
			when RCova::Covered
				@covered += 1
			when RCova::Uncovered
				@uncovered += 1
			when RCova::Noop
				@noop += 1
			end
		end
		
		def done
			yield_lines_out
		end
		
		def method_missing name, *args
		end
	end
	
	
	class TaskInfo
		def initialize argv
			@dir = ""
			@file = ""
			@modules = []
	
			@exe_name = argv[0]
			@exe_name = @exe_name + ".exe" unless @exe_name =~ /.*\.(exe|bat|cmd)$/i
			raise FatalError, "exe file not found: #{@exe_name}" unless File.exist? @exe_name and not File.directory? @exe_name
			
			
			mods = argv[1..-1]
			raise FatalError, "must give at least one module please" if not @exe_name =~ /.*\.exe$/i and mods.empty?
			mods << @exe_name if mods.empty?
			
			mods.each do |m|
				m = m + ".dll" unless File.basename(m) =~ /\./
				raise FatalError, "module not found: #{m}" unless File.exist? m and not File.directory? m
				@modules << m
			end
			
			basename = File.basename @modules.first
			ext = File.extname @modules.first
			
			@dir = @modules.first[0..-(basename.size+1)]
			@file = basename[0..-(ext.size+1)]
			
		end
		
		def name
			@dir.empty? ? @file : File.join(@dir, @file)
		end
		
		attr_reader :dir, :modules, :file, :exe_name
		
		def cmd_pre_prep
			"prep /lv /om #{modules.map{|m| m +' '} }"[0..-2]
		end
		
		def cmd_profile
			"profile /i #{name} /o #{name} #{exe_name}"
		end
		
		def cmd_post_prep
			"prep /m #{name}"
		end
		
		def cmd_plist
			"plist #{name}"
		end
		
	end
	
	def self.helper_guarded_exec cmd, title = nil
		STDERR.puts "#{'='*20} #{title} #{'='*20}" if title and not title.empty?
		raise FatalError, "failed to exec #{cmd}" unless system(cmd)
		true
	end
	
	def self.run_task task_info, profile_report
		begin
			helper_guarded_exec task_info.cmd_pre_prep, "pre-PREP"
			helper_guarded_exec "#{task_info.cmd_profile} 1>&2", "PROFILE"
			helper_guarded_exec task_info.cmd_post_prep, "post-PREP"
			if profile_report then
				helper_guarded_exec "#{task_info.cmd_plist} 1>#{profile_report}", "PLIST"
			else
				helper_guarded_exec task_info.cmd_plist, "PLIST"
			end
		ensure
			%x{del #{task_info.name}.pb? 1>&2 2>nul}
		end
	end
	
	def self.run_html_report argv, excludes, erb_index, erb_file, output_dir = nil
		begin
			run_task TaskInfo.new(argv), '_profile'
			html_report '_profile', excludes, erb_index, erb_file, output_dir
		ensure
			%x{del _profile 2>&1 1>nul}
		end
	end
	
	def self.parse profile_report, excludes
		modules = {}

		feed = ParseFeed.new excludes do |file|
			yield file if block_given?

			name = file[:module]
			modules[name] = {:files => []} if not modules[name]
			
			file.delete(:text)
			file.delete(:module)
			file.delete(:lines)
			modules[name][:files] << file
			
		end 
		
		if not File.exist? profile_report
			raise FatalError, "Can not find profile result: \"#{profile_report}\", please check it." 
		end
		cov = RCova::Coverage.analyse(profile_report, feed)
		
		stat_total = 0
		stat_coverage = 0
		modules.each do |name, m|
			total = 0
			coverage = 0
			m[:files].each do |f|
				total += f[:total]
				coverage += f[:coverage]
			end
			m[:total] = total
			m[:coverage] = coverage / m[:files].size
			m[:name] = name
			stat_total += m[:total]
			stat_coverage += m[:coverage]
		end
		
		raise FatalError, "Illegal pdb(s) file, rebuild your module(s) with profile setting(in link page) enabled." if modules.empty?
		{ 	:coverage => stat_coverage / modules.size, 
			:total => stat_total, 
			:modules => modules.values,
			:date => cov.date,
			:command_line_at => cov.command_line_at }
	end
	
	def self.helper_convert_path_to_filename path
		ret = path.dup
		ret = ret[3..-1] if ret =~ /^\w:[\\\/]/
		ret.gsub!(/[\\\/]/, '-')
		ret.gsub!(/\./, '_')
		'files/' + ret + '.html'
	end
	
	def self.html_report profile_report, excludes, erb_index, erb_file, output_dir = nil
		output_dir = 'coverage' unless output_dir
		Dir.mkdir output_dir if not File.exist? output_dir
		Dir.mkdir "#{output_dir}/files" if not File.exist? "#{output_dir}/files"
		
		stat = parse profile_report, excludes do |file|
			fname = "#{output_dir}/#{helper_convert_path_to_filename(file[:name])}"
			open fname, 'w' do |f|
				f.write erb_file.result(binding)
			end
		end
		
		open 'coverage/index.html', 'w' do |f|
			f.write erb_index.result(binding)
		end
		
	end

	module Html
		def self.render_line line, base_lineno = 0
			
			class_name = ""
			class_odd_even = ""
			case line[:status]
			when RCova::Covered
				class_name = 'covered'
			when RCova::Uncovered
				class_name = 'uncovered'
			when RCova::Noop
				class_name = 'noop'
			end
			class_odd_even = "#{class_name}-#{(line[:lineno]%2 == 0) ? 'even' : 'odd'}"
			
			text = line[:text].empty? ? "&nbsp;" : line[:text]
			text.gsub!(/</, '&lt;')
			text.gsub!(/>/, '&gt;')
			
			"<div class='#{class_name} #{class_odd_even}'><pre>#{sprintf("%4d", line[:lineno] - base_lineno + 1)}:  #{text}</pre></div>"
		end
		
		def self.cov_to_percent cov
			"#{cov / 100}%" 
		end
		
		def self.link_to_file name
			"<a href='#{QCov::Profile::helper_convert_path_to_filename name}'>#{name}</a>"
		end
		
		def self.percent_bar percent
			"<table border='0'><tr>
			<td><div class='percent_bar'><div class='indicator' style='width:#{Html::cov_to_percent percent}'></div></div></td>
			<td>#{Html::cov_to_percent percent}</td>
			</tr></table>"
		end
	end

end

end
